CDKでデプロイしたLambda Functionsで `Cannot find module '@smithy/service-error-classification'`が発生したので対応した話
リテールアプリ共創部@大阪の岩田です。
とあるプロジェクトでCDKでデプロイしたNode.jsのLambda Functionsで Cannot find module '@smithy/service-error-classification'
というエラーが発生したので対応しました。内容的に他にもハマる人が出てきそうなのでブログで共有します。
先に結論
CDKのバージョンv2.161.0から@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages
という設定値が追加されました。この設定値はデフォルトでtrueとなっており、特定の条件下(※後述します)でcdk deploy
するとesbuildの引数に --external:@smithy/*
が追加され、@smithy/service-error-classification
はバンドル対象外になります。そのためaws-xray-core
を利用している場合はCannot find module '@smithy/service-error-classification
のエラーが発生することになります。
対策ですが@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages
をfalseに設定する...のではなくbundleAwsSDK
をtrueに設定するのが良いでしょう。
環境
今回利用した各種ライブラリのバージョンは以下のとおりです。
- aws-cdk-lib: 2.172.0
- aws-xray-sdk-core: 3.10.2
- aws-xray-sdk-fetch: 3.10.2
経緯など
ここからはエラーが発生するようになった経緯と調査の過程について共有します。
aws-xray-sdk-fetch
を追加
対象のLambda FunctionはBFF的な処理を行うものでした。fetch
を利用した外部APIの呼び出しが多いため、オブザーバビリティを担保するために aws-xray-sdk-fetch
を導入して外部APIの呼び出しをトレースできるようにした方が良いという話をし、aws-xray-sdk-fetch
を導入することになりました。
以下のようなイメージです。
import { captureFetchGlobal } from 'aws-xray-sdk-fetch';
const capturedFetch = captureFetchGlobal()
...略
const res = await capturedFetch('https://dev.classmethod.jp')
これでfetchがトレース可能になりました!
cdk deployすると...動かない
トレースを仕込んだのでcdk deploy
すると...Lambdaが動きません。以下のようなエラーログが出力されており、 @smithy/service-error-classification
が見つからないようです。
2024-12-17T03:27:58.686Z undefined ERROR Uncaught Exception {
"errorType": "Runtime.ImportModuleError",
"errorMessage": "Error: Cannot find module '@smithy/service-error-classification'\nRequire stack:\n- /var/task/index.js\n- /var/runtime/index.mjs",
"stack": [
"Runtime.ImportModuleError: Error: Cannot find module '@smithy/service-error-classification'",
"Require stack:",
"- /var/task/index.js",
"- /var/runtime/index.mjs",
" at _loadUserApp (file:///var/runtime/index.mjs:1087:17)",
" at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
" at async start (file:///var/runtime/index.mjs:1282:23)",
" at async file:///var/runtime/index.mjs:1288:1"
]
}
aws-xray-sdk-fetch
はaws-xray-sdk-core
に依存しており、aws-xray-sdk-core
は@smithy/service-error-classification
に依存しているのですが、なぜ@smithy/service-error-classification
が見つからないのか...
@smithy/service-error-classification
が含まれていない
cdk synthで生成されるアセットに 原因の切り分けとして aws-xray-sdk-fetch
を利用している他のプロジェクトと比較したところ、cdk synth
で生成されるcdk.out/asset...略/index.js
というアセットがおかしいことが分かりました。正常に動作しているプロジェクトではアセットの中身が以下のようになっていました。
// node_modules/@smithy/service-error-classification/dist-cjs/index.js
var require_dist_cjs = __commonJS({
"node_modules/@smithy/service-error-classification/dist-cjs/index.js"(exports2, module2) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp2(target, "name", { value, configurable: true });
var __export2 = (target, all) => {
for (var name in all)
__defProp2(target, name, { get: all[name], enumerable: true });
};
...略
@smithy/service-error-classification
の中身がバンドルされていることが分かります。一方でエラーが発生しているプロジェクトではアセット内に上記のようなコードが出力されていませんでした。何かしらの要因で @smithy/service-error-classification
がバンドル対象外となっているようです。
externalModulesは指定していないが...??
対象のLambda FunctionをデプロイするCDKコードを確認したところ、以下のような何の変哲も無い実装でした。
const restApiFunc = new aws_lambda_nodejs.NodejsFunction(
this,
'RestApiFunc',
{
architecture: aws_lambda.Architecture.ARM_64,
runtime: aws_lambda.Runtime.NODEJS_22_X,
entry: 'index.ts',
memorySize: 1769,
timeout: Duration.seconds(29),
tracing: aws_lambda.Tracing.ACTIVE,
},
);
てっきりexternalModules
で@smithy/service-error-classification
が指定されているのかと思いましたがそうでは無さそうです。なぜ...
esbuildを手動実行するとバンドルされた
原因切り分けのためにCDKを介さず手動で./node_modules/.bin/esbuild --platform=node --bundle src/index.ts
を実行したところ、今度は無事に@smithy/service-error-classification
がバンドルされました。ということはCDKからesbuildを呼び出すあたりに原因がありそうです。
ログを仕込む
CDKがesbuildをどのように呼び出してるのか特定したかったので、力技で node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.js
のコードにログ出力のロジックを追加しました。
// ...略
if(!Bundling.esbuildInstallation.version.startsWith(`${ESBUILD_MAJOR_VERSION}.`))throw new Error(`Expected esbuild version ${ESBUILD_MAJOR_VERSION}.x but got ${Bundling.esbuildInstallation.version}`);
const localCommand=createLocalCommand(outputDir,Bundling.esbuildInstallation,Bundling.tscInstallation);
// 追加↓
console.error(localCommand);
// 追加↑
return(0,util_1().exec)(osPlatform==="win32"?"cmd":"bash",[osPlatform==="win32"?"/c":"-c",localCommand],{env:{...process.env,...environment},stdio:["ignore",process.stderr,"inherit"],cwd,windowsVerbatimArguments:osPlatform==="win3
node_modules
の中身なのでトランスパイルされており、編集するのに気を使いました。この状態でcdk synth
を実行すると以下のような出力が得られました。
npx --no-install esbuild --bundle "...略/index.ts" --target=node22 --platform=node --outfile="...略/cdk.out/bundling-temp-...略/index.js" --external:@aws-sdk/* --external:@smithy/*
esbuildのオプションに--external:@smithy/*
が設定されていることが分かります。ついでに言うと--external:@aws-sdk/*
も設定されていますね。
CDKのコードを確認
大体原因にあたりがついたのでCDKのコードを確認します。
この辺りでexternal
に@smithy/*
を指定しているようです。
cdk.FeatureFlags.of(scope).isEnabled(LAMBDA_NODEJS_SDK_V3_EXCLUDE_SMITHY_PACKAGES)
でチェックしていることからcdk.json
の@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages
をtrueに設定すれば良さそうですね。ここを修正して再度cdk synth
すると無事に@smithy/service-error-classification
もバンドルされるようになりました🎉
コミット履歴を調べると上記の設定値は以下のコミットで追加されたようです。
元になったissueは以下です。
AWS SDKはバンドル対象外になっているのに@smithy
系のパッケージがバンドルされていたため、Lambda実行環境のAWS CDKとバンドルした@smithy
系の互換性の問題でエラーが発生していたそうです。
そしてこのissueを見て気付いたのですが、NodejsFunction
のデフォルト設定を使うとAWS SDKはバンドルされないんですね...
Lambdaの公式ドキュメントには以下のように記載されており、Lambda実行環境に組み込まれているAWS SDKを使うことのリスクについて示唆しています。
Control the dependencies in your function's deployment package. The AWS Lambda execution environment contains a number of libraries. For the Node.js and Python runtimes, these include the AWS SDKs. To enable the latest set of features and security updates, Lambda will periodically update these libraries. These updates may introduce subtle changes to the behavior of your Lambda function. To have full control of the dependencies your function uses, package all of your dependencies with your deployment package.
ということで @smithy
系のパッケージだけでなくAWS SDKについてもバンドル対象とするようにNodejsFunction
の呼び出しを修正しました。
const restApiFunc = new aws_lambda_nodejs.NodejsFunction(
this,
'RestApiFunc',
{
architecture: aws_lambda.Architecture.ARM_64,
runtime: aws_lambda.Runtime.NODEJS_22_X,
entry: 'index.ts',
bundling: {
bundleAwsSDK: true,
},
memorySize: 1769,
timeout: Duration.seconds(29),
tracing: aws_lambda.Tracing.ACTIVE,
},
);
bundleAwsSDK
がtrueの場合はdefaultExternals
が[]
になるようです。
なのでbundleAwsSDK
にtrueを指定している場合はcdk.json
の@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages
はtrue/falseどっちでも問題無さそうですね。ということで記事の冒頭に記載した「特定の条件下」というのはbundleAwsSDK
が未指定もしくはfalseの場合になります。
まとめ
Cannot find module '@smithy/service-error-classification'
のエラーを解消した経緯についてご紹介しました。個人的にはNodejsFunction
がデフォルトでAWS CDKをバンドルしないというのが衝撃でした。これまでデプロイしてきたLambdaのAWS CDKが意図通りのバージョンになっていない可能性が高そうなので改めてチェックしてみます。
参考
- (aws-lambda-nodejs): treat
@smithy
scope identically to@aws-sdk
scope if excluding packages · Issue #31610 · aws/aws-cdk - fix(lambda-nodejs): remove smithy models from bundling for AWS SDK v3… · aws/aws-cdk@19ee46d
- fix(lambda-nodejs): support bundling aws-sdk as part of the bundled c… · aws/aws-cdk@2378635
- aws_lambda_nodejs: Using Lambda Provided SDK by default in NodejsFunction leads to higher cold starts · Issue #25492 · aws/aws-cdk
- Define Lambda function handler in Node.js - AWS Lambda